/******************************************************************************
 * Copyright (c) 2002, 2003 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    IBM Corporation - initial API and implementation 
 ****************************************************************************/

package com.cea.papyrus.diagram.sequence.sedi.figures;

/*
 * Note: This class is based on the class with the same name in the GMF 1.0.3 release. The
 * class is replicated in order to avoid a dependency on the massive GMF framework for
 * only one small class. The original package is
 * org.eclipse.gmf.runtime.draw2d.ui.figures.
 */

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.eclipse.draw2d.IFigure;
import org.eclipse.draw2d.ToolbarLayout;
import org.eclipse.draw2d.geometry.Dimension;
import org.eclipse.draw2d.geometry.Insets;
import org.eclipse.draw2d.geometry.Rectangle;

/**
 * An extended toolbar layout that supports the following additional features: 1- The
 * ability to stretch the major axis 2- The ability to reverse the children in layout 3-
 * The ability to ignore invisible children 4- The ability to set ratio constraints on
 * children (in major axis)
 * 
 * @author melaasar
 */
public class ConstrainedToolbarLayout extends ToolbarLayout {

    /**
     * Whether to stretch the major axis
     */
    private boolean stretchMajorAxis = true;
    /**
     * Alignment along major axis
     */
    private int majorAlignment = ToolbarLayout.ALIGN_TOPLEFT;
    /**
     * Whether to reverse children for layout
     */
    private boolean reversed = false;
    /**
     * Whether to ignore invisible children
     */
    private boolean ignoreInvisibleChildren = true;
    /**
     * The constrains map
     */
    private Map<Object, Object> constraints;

    /**
     * Creates a new vertical ConstrainedToolbarLayout
     */
    public ConstrainedToolbarLayout() {
        this(false);
    }

    /**
     * Creates a new ConstrainedToolbarLayout with a given orientation
     * 
     * @param isHorizontal
     *            Whether the layout is horizontal
     */
    public ConstrainedToolbarLayout(boolean isHorizontal) {
        super(isHorizontal);
        setStretchMinorAxis(true);
        setStretchMajorAxis(true);
        setMinorAlignment(ALIGN_CENTER);
    }

    public void setMajorAlignment(int alignment) {
        if (alignment != ToolbarLayout.ALIGN_TOPLEFT && alignment != ToolbarLayout.ALIGN_CENTER
                && alignment != ToolbarLayout.ALIGN_BOTTOMRIGHT)
            throw new IllegalArgumentException("alignment: " + alignment);

        this.majorAlignment = alignment;
    }

    public int getMajorAlignment() {
        return this.majorAlignment;
    }

    /**
     * @see org.eclipse.draw2d.AbstractLayout#calculatePreferredSize(org.eclipse.draw2d.IFigure,
     *      int, int)
     */
    protected Dimension calculatePreferredSize(IFigure container, int wHint, int hHint) {
        Insets insets = container.getInsets();
        if (!container.isVisible())
            return new Dimension(insets.getWidth(), insets.getHeight());
        if (isHorizontal()) {
            wHint = -1;
            if (hHint >= 0)
                hHint = Math.max(0, hHint - insets.getHeight());
        } else {
            hHint = -1;
            if (wHint >= 0)
                wHint = Math.max(0, wHint - insets.getWidth());
        }

        List<?> children = getChildren(container);
        Dimension prefSize = calculateChildrenSize(children, wHint, hHint, true);
        // Do a second pass, if necessary
        if (wHint >= 0 && prefSize.width > wHint) {
            prefSize = calculateChildrenSize(children, prefSize.width, hHint, true);
        } else if (hHint >= 0 && prefSize.width > hHint) {
            prefSize = calculateChildrenSize(children, wHint, prefSize.width, true);
        }

        prefSize.height += Math.max(0, children.size() - 1) * spacing;
        return transposer.t(prefSize).expand(insets.getWidth(), insets.getHeight()).union(
                getBorderPreferredSize(container));
    }

    /**
     * @see org.eclipse.draw2d.AbstractHintLayout#calculateMinimumSize(org.eclipse.draw2d.IFigure,
     *      int, int)
     */
    public Dimension calculateMinimumSize(IFigure container, int wHint, int hHint) {
        Insets insets = container.getInsets();
        if (!container.isVisible())
            return new Dimension(insets.getWidth(), insets.getHeight());

        if (isHorizontal()) {
            wHint = -1;
            if (hHint >= 0)
                hHint = Math.max(0, hHint - insets.getHeight());
        } else {
            hHint = -1;
            if (wHint >= 0)
                wHint = Math.max(0, wHint - insets.getWidth());
        }

        List<?> children = getChildren(container);
        Dimension minSize = calculateChildrenSize(children, wHint, hHint, false);
        // Do a second pass, if necessary
        if (wHint >= 0 && minSize.width > wHint) {
            minSize = calculateChildrenSize(children, minSize.width, hHint, false);
        } else if (hHint >= 0 && minSize.width > hHint) {
            minSize = calculateChildrenSize(children, wHint, minSize.width, false);
        }

        minSize.height += Math.max(0, children.size() - 1) * spacing;
        return transposer.t(minSize).expand(insets.getWidth(), insets.getHeight()).union(
                getBorderPreferredSize(container));
    }

    /**
     * @see org.eclipse.draw2d.LayoutManager#layout(IFigure)
     */
    public void layout(IFigure parent) {
        if (!parent.isVisible())
            return;
        List<?> children = getChildren(parent);
        int numChildren = children.size();
        Rectangle clientArea = transposer.t(parent.getClientArea());
        int x = clientArea.x;
        int y = clientArea.y;
        int availableHeight = clientArea.height;

        Dimension prefSizes[] = new Dimension[numChildren];
        Dimension minSizes[] = new Dimension[numChildren];
        Dimension maxSizes[] = new Dimension[numChildren];

        // Calculate the width and height hints. If it's a vertical ToolBarLayout,
        // then ignore the height hint (set it to -1); otherwise, ignore the
        // width hint. These hints will be passed to the children of the parent
        // figure when getting their preferred size.
        int wHint = -1;
        int hHint = -1;
        if (isHorizontal()) {
            hHint = parent.getClientArea(Rectangle.SINGLETON).height;
        } else {
            wHint = parent.getClientArea(Rectangle.SINGLETON).width;
        }

        /*
         * Calculate sum of preferred heights of all children(totalHeight). Calculate sum
         * of minimum heights of all children(minHeight). Cache Preferred Sizes and
         * Minimum Sizes of all children. totalHeight is the sum of the preferred heights
         * of all children totalMinHeight is the sum of the minimum heights of all
         * children prefMinSumHeight is the sum of the difference between all children's
         * preferred heights and minimum heights. (This is used as a ratio to calculate
         * how much each child will shrink).
         */
        IFigure child;
        int totalHeight = 0;
        int totalMinHeight = 0;
        double totalMaxHeight = 0;
        int prefMinSumHeight = 0;
        double prefMaxSumHeight = 0;

        for (int i = 0; i < numChildren; i++) {
            child = (IFigure) children.get(i);

            prefSizes[i] = transposer.t(child.getPreferredSize(wHint, hHint));
            minSizes[i] = transposer.t(child.getMinimumSize(wHint, hHint));
            maxSizes[i] = transposer.t(child.getMaximumSize());

            if (getConstraint(child) != null) {
                double ratio = ((Double) getConstraint(child)).doubleValue();
                int prefHeight = (int) (ratio * availableHeight);
                prefHeight = Math.max(prefHeight, minSizes[i].height);
                prefHeight = Math.min(prefHeight, maxSizes[i].height);
                prefSizes[i].height = prefHeight;
            }

            totalHeight += prefSizes[i].height;
            totalMinHeight += minSizes[i].height;
            totalMaxHeight += maxSizes[i].height;
        }
        totalHeight += (numChildren - 1) * spacing;
        totalMinHeight += (numChildren - 1) * spacing;
        totalMaxHeight += (numChildren - 1) * spacing;
        prefMinSumHeight = totalHeight - totalMinHeight;
        prefMaxSumHeight = totalMaxHeight - totalHeight;

        /*
         * The total amount that the children must be shrunk is the sum of the preferred
         * Heights of the children minus Max(the available area and the sum of the minimum
         * heights of the children). amntShrinkHeight is the combined amount that the
         * children must shrink amntShrinkCurrentHeight is the amount each child will
         * shrink respectively
         */
        int amntShrinkHeight = totalHeight - Math.max(availableHeight, totalMinHeight);

        int majorAdjust = (availableHeight - totalHeight);
        switch (getMajorAlignment()) {
        case ALIGN_TOPLEFT:
            majorAdjust = 0;
            break;
        case ALIGN_CENTER:
            majorAdjust /= 2;
            break;
        case ALIGN_BOTTOMRIGHT:
            break;
        }
        y += majorAdjust;

        for (int i = 0; i < numChildren; i++) {
            int amntShrinkCurrentHeight = 0;
            int prefHeight = prefSizes[i].height;
            int minHeight = minSizes[i].height;
            int maxHeight = maxSizes[i].height;
            int prefWidth = prefSizes[i].width;
            int minWidth = minSizes[i].width;
            int maxWidth = maxSizes[i].width;
            Rectangle newBounds = new Rectangle(x, y, prefWidth, prefHeight);

            child = (IFigure) children.get(i);
            if (getStretchMajorAxis()) {
                if (amntShrinkHeight > 0 && prefMinSumHeight != 0)
                    amntShrinkCurrentHeight =
                            (prefHeight - minHeight) * amntShrinkHeight / (prefMinSumHeight);
                else if (amntShrinkHeight < 0 && totalHeight != 0)
                    amntShrinkCurrentHeight =
                            (int) (((maxHeight - prefHeight) / prefMaxSumHeight) * amntShrinkHeight);
            }

            int width = Math.min(prefWidth, maxWidth);
            if (matchWidth)
                width = maxWidth;
            width = Math.max(minWidth, Math.min(clientArea.width, width));
            newBounds.width = width;

            int adjust = clientArea.width - width;
            switch (minorAlignment) {
            case ALIGN_TOPLEFT:
                adjust = 0;
                break;
            case ALIGN_CENTER:
                adjust /= 2;
                break;
            case ALIGN_BOTTOMRIGHT:
                break;
            }
            newBounds.x += adjust;
            if (newBounds.height - amntShrinkCurrentHeight > maxHeight)
                amntShrinkCurrentHeight = newBounds.height - maxHeight;
            newBounds.height -= amntShrinkCurrentHeight;
            child.setBounds(transposer.t(newBounds));

            amntShrinkHeight -= amntShrinkCurrentHeight;
            prefMinSumHeight -= (prefHeight - minHeight);
            prefMaxSumHeight -= (maxHeight - prefHeight);
            totalHeight -= prefHeight;
            y += newBounds.height + spacing;
        }
    }

    /**
     * @see org.eclipse.draw2d.LayoutManager#getConstraint(org.eclipse.draw2d.IFigure)
     */
    public Object getConstraint(IFigure child) {
        if (constraints != null)
            return constraints.get(child);
        return null;
    }

    /**
     * @see org.eclipse.draw2d.LayoutManager#setConstraint(org.eclipse.draw2d.IFigure,
     *      java.lang.Object)
     */
    @SuppressWarnings("unchecked")
    public void setConstraint(IFigure child, Object constraint) {
        if (!(constraint instanceof Double))
            return;
        if (constraint instanceof Double) {
            Double c = (Double) constraint;
            super.setConstraint(child, constraint);
            if (constraints == null)
                constraints = new HashMap();
            if (constraint == null || c.doubleValue() <= 0) {
                if (constraints.containsKey(child))
                    constraints.remove(child);
            } else
                constraints.put(child, constraint);
        }
    }

    /**
     * @see org.eclipse.draw2d.LayoutManager#remove(org.eclipse.draw2d.IFigure)
     */
    public void remove(IFigure child) {
        super.remove(child);
        setConstraint(child, null);
    }

    /**
     * Sets whether to stretch the major axis or not
     * 
     * @param stretch
     *            Whether to stretch the major axis or not
     */
    public void setStretchMajorAxis(boolean stretch) {
        stretchMajorAxis = stretch;
    }

    /**
     * @return Whether the stretch major axis is on
     */
    public boolean getStretchMajorAxis() {
        return stretchMajorAxis;
    }

    /**
     * Sets whether to reverse children or not
     * 
     * @param reversed
     *            Whether to reverse children or not
     */
    public void setReversed(boolean reversed) {
        this.reversed = reversed;
    }

    /**
     * @return Whether the reverse children or not
     */
    public boolean isReversed() {
        return reversed;
    }

    /**
     * Sets whether to ignore invisible children or not
     * 
     * @param ignoreInvisibleChildren
     *            Whether to ignore invisible children or not
     */
    public void setIgnoreInvisibleChildren(boolean ignoreInvisibleChildren) {
        this.ignoreInvisibleChildren = ignoreInvisibleChildren;
    }

    /**
     * @return Whether to ignore invisible children or not
     */
    public boolean getIgnoreInvisibleChildren() {
        return ignoreInvisibleChildren;
    }

    /**
     * Calculates either the preferred or minimum children size
     */
    private Dimension calculateChildrenSize(List<?> children, int wHint, int hHint, boolean preferred) {
        Dimension childSize;
        IFigure child;
        int height = 0, width = 0;
        for (int i = 0; i < children.size(); i++) {
            child = (IFigure) children.get(i);
            childSize =
                    transposer.t(preferred ? child.getPreferredSize(wHint, hHint) : child
                            .getMinimumSize(wHint, hHint));
            height += childSize.height;
            width = Math.max(width, childSize.width);
        }
        return new Dimension(width, height);
    }

    /**
     * Gets the list of children after applying the layout options of ignore invisible
     * children & reverse children
     */
    @SuppressWarnings("unchecked")
    private List getChildren(IFigure container) {
        List children = new ArrayList(container.getChildren());
        if (getIgnoreInvisibleChildren()) {
            Iterator iter = children.iterator();
            while (iter.hasNext()) {
                IFigure f = (IFigure) iter.next();
                if (!f.isVisible())
                    iter.remove();
            }
        }
        if (isReversed())
            Collections.reverse(children);
        return children;
    }

}
